use actix_http::StatusCode;
use actix_web::test;
use futures::StreamExt;
use labrinth::models::projects::VersionId;
use labrinth::{
    models::projects::{Loader, VersionStatus, VersionType},
    routes::v2::version_file::FileUpdateData,
};
use serde_json::json;

use crate::assert_status;
use crate::common::api_common::{ApiProject, ApiVersion};
use crate::common::api_v2::ApiV2;

use crate::common::api_v2::request_data::get_public_project_creation_data;
use crate::common::dummy_data::{DummyProjectAlpha, DummyProjectBeta};
use crate::common::environment::{with_test_environment, TestEnvironment};
use crate::common::{
    database::{ENEMY_USER_PAT, USER_USER_PAT},
    dummy_data::TestFile,
};

#[actix_rt::test]
pub async fn test_patch_version() {
    with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
        let api = &test_env.api;

        let alpha_version_id = &test_env.dummy.project_alpha.version_id;

        // // First, we do some patch requests that should fail.
        // // Failure because the user is not authorized.
        let resp = api
            .edit_version(
                alpha_version_id,
                json!({
                    "name": "test 1",
                }),
                ENEMY_USER_PAT,
            )
            .await;
        assert_status!(&resp, StatusCode::UNAUTHORIZED);

        // Failure because these are illegal requested statuses for a normal user.
        for req in ["unknown", "scheduled"] {
            let resp = api
                .edit_version(
                    alpha_version_id,
                    json!({
                        "status": req,
                        // requested status it not set here, but in /schedule
                    }),
                    USER_USER_PAT,
                )
                .await;
            assert_status!(&resp, StatusCode::BAD_REQUEST);
        }

        // Sucessful request to patch many fields.
        let resp = api
            .edit_version(
                alpha_version_id,
                json!({
                    "name": "new version name",
                    "version_number": "1.3.0",
                    "changelog": "new changelog",
                    "version_type": "beta",
                    // // "dependencies": [], TODO: test this
                    "game_versions": ["1.20.5"],
                    "loaders": ["forge"],
                    "featured": false,
                    // "primary_file": [], TODO: test this
                    // // "downloads": 0, TODO: moderator exclusive
                    "status": "draft",
                    // // "filetypes": ["jar"], TODO: test this
                }),
                USER_USER_PAT,
            )
            .await;
        assert_status!(&resp, StatusCode::NO_CONTENT);

        let version = api
            .get_version_deserialized(alpha_version_id, USER_USER_PAT)
            .await;
        assert_eq!(version.name, "new version name");
        assert_eq!(version.version_number, "1.3.0");
        assert_eq!(version.changelog, "new changelog");
        assert_eq!(
            version.version_type,
            serde_json::from_str::<VersionType>("\"beta\"").unwrap()
        );
        assert_eq!(version.game_versions, vec!["1.20.5"]);
        assert_eq!(version.loaders, vec![Loader("forge".to_string())]);
        assert!(!version.featured);
        assert_eq!(version.status, VersionStatus::from_string("draft"));

        // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions'
        // works as expected, as well as only 'loaders'
        let resp = api
            .edit_version(
                alpha_version_id,
                json!({
                    "game_versions": ["1.20.1", "1.20.2", "1.20.4"],
                }),
                USER_USER_PAT,
            )
            .await;
        assert_status!(&resp, StatusCode::NO_CONTENT);

        let version = api
            .get_version_deserialized(alpha_version_id, USER_USER_PAT)
            .await;
        assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]);
        assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch

        let resp = api
            .edit_version(
                alpha_version_id,
                json!({
                    "loaders": ["fabric"],
                }),
                USER_USER_PAT,
            )
            .await;
        assert_status!(&resp, StatusCode::NO_CONTENT);

        let version = api
            .get_version_deserialized(alpha_version_id, USER_USER_PAT)
            .await;
        assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); // From last patch
        assert_eq!(version.loaders, vec![Loader("fabric".to_string())]);
    })
    .await;
}

#[actix_rt::test]
async fn version_updates() {
    // Test setup and dummy data
    with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
        let api = &test_env.api;
        let DummyProjectAlpha {
            project_id: alpha_project_id,
            project_id_parsed: alpha_project_id_parsed,
            version_id: alpha_version_id,
            file_hash: alpha_version_hash,
            ..
        } = &test_env.dummy.project_alpha;
        let DummyProjectBeta {
            version_id: beta_version_id,
            file_hash: beta_version_hash,
            ..
        } = &test_env.dummy.project_beta;

        // Quick test, using get version from hash
        let version = api
            .get_version_from_hash_deserialized(alpha_version_hash, "sha1", USER_USER_PAT)
            .await;
        assert_eq!(&version.id.to_string(), alpha_version_id);

        // Get versions from hash
        let versions = api
            .get_versions_from_hashes_deserialized(
                &[alpha_version_hash.as_str(), beta_version_hash.as_str()],
                "sha1",
                USER_USER_PAT,
            )
            .await;
        assert_eq!(versions.len(), 2);
        assert_eq!(
            &versions[alpha_version_hash].id.to_string(),
            alpha_version_id
        );
        assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id);

        // When there is only the one version, there should be no updates
        let version = api
            .get_update_from_hash_deserialized_common(
                alpha_version_hash,
                "sha1",
                None,
                None,
                None,
                USER_USER_PAT,
            )
            .await;
        assert_eq!(&version.id.to_string(), alpha_version_id);

        let versions = api
            .update_files_deserialized_common(
                "sha1",
                vec![alpha_version_hash.to_string()],
                None,
                None,
                None,
                USER_USER_PAT,
            )
            .await;
        assert_eq!(versions.len(), 1);
        assert_eq!(
            &versions[alpha_version_hash].id.to_string(),
            alpha_version_id
        );

        // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders
        let mut update_ids = vec![];
        for (version_number, patch_value) in [
            (
                "0.9.9",
                json!({
                    "game_versions": ["1.20.1"],
                }),
            ),
            (
                "1.5.0",
                json!({
                    "game_versions": ["1.20.3"],
                    "loaders": ["fabric"],
                }),
            ),
            (
                "1.5.1",
                json!({
                    "game_versions": ["1.20.4"],
                    "loaders": ["forge"],
                    "version_type": "beta"
                }),
            ),
        ]
        .iter()
        {
            let version = api
                .add_public_version_deserialized_common(
                    *alpha_project_id_parsed,
                    version_number,
                    TestFile::build_random_jar(),
                    None,
                    None,
                    USER_USER_PAT,
                )
                .await;
            update_ids.push(version.id);

            // Patch using json
            api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT)
                .await;
        }

        let check_expected = |game_versions: Option<Vec<String>>,
                              loaders: Option<Vec<String>>,
                              version_types: Option<Vec<String>>,
                              result_id: Option<VersionId>| async move {
            let (success, result_id) = match result_id {
                Some(id) => (true, id),
                None => (false, VersionId(0)),
            };
            // get_update_from_hash
            let resp = api
                .get_update_from_hash(
                    alpha_version_hash,
                    "sha1",
                    loaders.clone(),
                    game_versions.clone(),
                    version_types.clone(),
                    USER_USER_PAT,
                )
                .await;
            if success {
                assert_status!(&resp, StatusCode::OK);
                let body: serde_json::Value = test::read_body_json(resp).await;
                let id = body["id"].as_str().unwrap();
                assert_eq!(id, &result_id.to_string());
            } else {
                assert_status!(&resp, StatusCode::NOT_FOUND);
            }

            // update_files
            let versions = api
                .update_files_deserialized_common(
                    "sha1",
                    vec![alpha_version_hash.to_string()],
                    loaders.clone(),
                    game_versions.clone(),
                    version_types.clone(),
                    USER_USER_PAT,
                )
                .await;
            if success {
                assert_eq!(versions.len(), 1);
                let first = versions.iter().next().unwrap();
                assert_eq!(first.1.id, result_id);
            } else {
                assert_eq!(versions.len(), 0);
            }

            // update_individual_files
            let hashes = vec![FileUpdateData {
                hash: alpha_version_hash.to_string(),
                loaders,
                game_versions,
                version_types: version_types.map(|v| {
                    v.into_iter()
                        .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap())
                        .collect()
                }),
            }];
            let versions = api
                .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT)
                .await;
            if success {
                assert_eq!(versions.len(), 1);
                let first = versions.iter().next().unwrap();
                assert_eq!(first.1.id, result_id);
            } else {
                assert_eq!(versions.len(), 0);
            }
        };

        let tests = vec![
            check_expected(
                Some(vec!["1.20.1".to_string()]),
                None,
                None,
                Some(update_ids[0]),
            ),
            check_expected(
                Some(vec!["1.20.3".to_string()]),
                None,
                None,
                Some(update_ids[1]),
            ),
            check_expected(
                Some(vec!["1.20.4".to_string()]),
                None,
                None,
                Some(update_ids[2]),
            ),
            // Loader restrictions
            check_expected(
                None,
                Some(vec!["fabric".to_string()]),
                None,
                Some(update_ids[1]),
            ),
            check_expected(
                None,
                Some(vec!["forge".to_string()]),
                None,
                Some(update_ids[2]),
            ),
            // Version type restrictions
            check_expected(
                None,
                None,
                Some(vec!["release".to_string()]),
                Some(update_ids[1]),
            ),
            check_expected(
                None,
                None,
                Some(vec!["beta".to_string()]),
                Some(update_ids[2]),
            ),
            // Specific combination
            check_expected(
                None,
                Some(vec!["fabric".to_string()]),
                Some(vec!["release".to_string()]),
                Some(update_ids[1]),
            ),
            // Impossible combination
            check_expected(
                None,
                Some(vec!["fabric".to_string()]),
                Some(vec!["beta".to_string()]),
                None,
            ),
            // No restrictions, should do the last one
            check_expected(None, None, None, Some(update_ids[2])),
        ];

        // Wait on all tests, 4 at a time
        futures::stream::iter(tests)
            .buffer_unordered(4)
            .collect::<Vec<_>>()
            .await;

        // We do a couple small tests for get_project_versions_deserialized as well
        // TODO: expand this more.
        let versions = api
            .get_project_versions_deserialized_common(
                alpha_project_id,
                None,
                None,
                None,
                None,
                None,
                None,
                USER_USER_PAT,
            )
            .await;
        assert_eq!(versions.len(), 4);
        let versions = api
            .get_project_versions_deserialized_common(
                alpha_project_id,
                None,
                Some(vec!["forge".to_string()]),
                None,
                None,
                None,
                None,
                USER_USER_PAT,
            )
            .await;
        assert_eq!(versions.len(), 1);
    })
    .await;
}

#[actix_rt::test]
async fn add_version_project_types_v2() {
    with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
        // Since v2 no longer keeps project_type at the project level but the version level,
        // we have to test that the project_type is set correctly when adding a version, if its done in separate requests.
        let api = &test_env.api;

        // Create a project in v2 with project_type = modpack, and no initial version set.
        let (test_project, test_versions) = api
            .add_public_project("test-modpack", None, None, USER_USER_PAT)
            .await;
        assert_eq!(test_versions.len(), 0); // No initial version set

        // Get as v2 project
        let test_project = api
            .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT)
            .await;
        assert_eq!(test_project.project_type, "project"); // No project_type set, as no versions are set
                                                          // Default to 'project' if none are found
                                                          // This is a known difference between older v2 ,but is acceptable.
                                                          // This would be the appropriate test on older v2:
                                                          // assert_eq!(test_project.project_type, "modpack");

        // Create a version with a modpack file attached
        let test_version = api
            .add_public_version_deserialized_common(
                test_project.id,
                "1.0.0",
                TestFile::build_random_mrpack(),
                None,
                None,
                USER_USER_PAT,
            )
            .await;

        // When we get the version as v2, it should display 'fabric' as the loader (and no project_type)
        let test_version = api
            .get_version_deserialized(&test_version.id.to_string(), USER_USER_PAT)
            .await;
        assert_eq!(test_version.loaders, vec![Loader("fabric".to_string())]);

        // When we get the project as v2, it should display 'modpack' as the project_type, and 'fabric' as the loader
        let test_project = api
            .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT)
            .await;
        assert_eq!(test_project.project_type, "modpack");
        assert_eq!(test_project.loaders, vec!["fabric"]);

        // When we get the version as v3, it should display 'mrpack' as the loader, and 'modpack' as the project_type
        // When we get the project as v3, it should display 'modpack' as the project_type, and 'mrpack' as the loader

        // The project should be a modpack project
    })
    .await;
}

#[actix_rt::test]
async fn test_incorrect_file_parts() {
    // Ensures that a version get that 'should' have mrpack_loaders does still display them
    //   if the file is 'mrpack' but the file_parts are incorrect
    with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
        let api = &test_env.api;

        // Patch to set the file_parts to something incorrect
        let patch = json!([{
            "op": "add",
            "path": "/file_parts",
            "value": ["invalid.zip"] // one file, wrong non-mrpack extension
        }]);

        // Create an empty project
        let slug = "test-project";
        let creation_data = get_public_project_creation_data(slug, None, None);
        let resp = api.create_project(creation_data, USER_USER_PAT).await;
        assert_status!(&resp, StatusCode::OK);

        // Get the project
        let project = api.get_project_deserialized(slug, USER_USER_PAT).await;
        assert_eq!(project.project_type, "project");

        // Create a version with a mrpack file, but incorrect file_parts
        let resp = api
            .add_public_version(
                project.id,
                "1.0.0",
                TestFile::build_random_mrpack(),
                None,
                Some(serde_json::from_value(patch).unwrap()),
                USER_USER_PAT,
            )
            .await;
        assert_status!(&resp, StatusCode::OK);

        // Get the project now, which should be now correctly identified as a modpack
        let project = api.get_project_deserialized(slug, USER_USER_PAT).await;
        assert_eq!(project.project_type, "modpack");
        assert_eq!(project.loaders, vec!["fabric"]);
    })
    .await;
}
